Configuring Sensors and Motors: Implementation Notes
This page provides design details on the (future) implementation of Configuring Sensors and Motors. As I have more likely overlooked some things, I'd be happy to hear about changes to the design on the mailing list.Class Diagram
Full size
SVG
Zargo file for use with ArgoUML
Overview
The EnchantingDeviceConfiguration is an base class that knows how the robotic system being programmed is configured. For the NXT, we'll use NXTConfiguration, which will keep track of the sensors, motors, and pilot for the NXT. (The pilot knows how the motors are set up and can drive the NXT around.)
The EnchantingDeviceConfiguration (or its subclass) can be asked to display an EnchantingConfigurationDialog with different blocks on it. Much like the 'Make a variable' and 'Make a list' buttons in the 'Variables' palette, the 'motion' palette will have 'Configure Motors' and 'Configure Pilot' buttons, and the 'Sensing' palette will have a 'Configure sensors' button. Pushing on these buttons will ask the NXTConfiguration to display a dialog appropriately set up for configuring the desired items.
The EnchantingConfiguration dialog, a very early version of which is shown above, is divided into two sections. The left-hand section is a parts palette filled with blocks that represent different peripherals you can configure for use (ConfigurationBlockMorphs) — such as different sensors, different motors, and different types of pilots. The right-hand section is a script editor that has hat blocks (ConfigurationHatMorphs — not in the UML diagram) representing the different ports of the NXT (or whatever device this happens to be). To indicate that you have a light sensor attached to port 3 of your NXT, you bring up the sensor configuration dialog, and drag a 'measures brightness' (which is what a light sensor does) configuration block from the palette and attach it to the hat block representing port 3. This diagram helps explain:
The ConfigurationBlockMorphs work very closely with EnchantingPeripheral and its subclasses. The Morph is the graphical representation of the peripheral, and the peripheral is the persistent representation of the morph.
Now for some further detail.
Notes on Smalltalk
The code will be written in (Squeak) Smalltalk. It is a beautiful object-oriented language, but has some syntactical and terminological differences that are worth noting (although they may be familiar if you know Objective-C).
In many languages, you invoke a member function/method/routine by calling it. In Smalltalk, you send a message.
In most languages, a declaration for a function with multiple parameters looks something like this:
void goToPosition(int x, int y)
In Smalltalk, the function would more likely be expressed as
goToX: x y: y
and would be written more compactly as goToX:y:. (It is very strange at first, but it grows on you). Also, the language is dynamically typed, so you never have to declare types.
In the UML diagram, I could not put colons in function names. If one would appear at the end, I've just omitted it; you know it is there by the fact that the function takes one or more parameters. If there is one in the middle, I put in an underscore.
Thus, on the chart, you'll see the function
initFieldsFrom_version(anObjStream : ObjStream,classVersion : byte)
This will actually be
initFieldsFrom: anObjStream version: classVersion
or
initFieldsFrom:version:for short. (The shortened form is also called a 'selector').
Lastly, the function names that has a + symbol in front are class functions. They are analogous to static functions in C++. In short, you can call them from anywhere without actually needing an object of that class to call them on.
I tend to write someclassname>>aSelector for regular (instance) functions, and someclassname class>>aSelector for class functions.
With that said, I hope you can follow along.
Launching the Dialog
The user presses the 'configure sensors' button, which sends NXTDeviceConfiguration>>runConfigurationDialog: 'sensors'. Creation of a EnchantingConfigurationDialog commences.
- The block palette is filled by calling NXTDeviceConfiguration class>>peripheralsFor: 'sensors', and iterating through the array of returned sensors classes (for example, #(NXTLightDetector NXTColorDetector NXTRangeDetector NXTSoundSensor ...)), sending createGenericConfigBlock to each class, and collecting the returned ConfigurationBlockMorphs as the contents of the palette. (To change the resulting morph, subclasses will want to override configBlockSpec, and not createGenericConfigBlock).
- The script editor is filled by:
- first, ConfigurationHatMorphs are created for each of the ports.
- then, the NXTDeviceConfiguration iterates through its list of sensors, and, for each one that is not nil, it sends EnchantingPeripheral>>createConfigBlock; the returned configuration block is attached to the port.
The dialog then runs, allowing the user to do there thing.
Concrete subtypes and substitutions
When the ConfigurationBlockMorphs are created, they are told what sort of EnchantingPeripheral created them (and store that in PeripheralClass). (This is so that, later, you can send ConfigurationBlockMorph>>createPeripheral and get a Peripheral set up according to the configuration.)
An ConfigurationBlockMorph might say something like:
Measures brightness Name: "light sensor" Uses an [NXT Color Sensor | NXT Light Sensor | RCX Light Sensor] Lamp is [off | on | red | blue | green | white]
While I haven't figured out how to adapt the BlockMorphs to take up multiple lines, assuming that a '/' indicates a newline, then this block will be created with a block specification (EnchantingPeripheral class>>configBlockSpec) something like this:
'Measures brightness/Name: %s/Uses an %P/Lamp is %J'
When a CommandBlockMorph (ie. a code block in the normal interface) is created, Enchanting parses the block specification and provides substitutions for all the parameters. Thus, the %s is replaced with an argument morph that allows you to enter text (in a function, not shown, called CommandBlockMorph>>uncoloredArgMorphFor: specString). With certain codes (such as %p, %P, %K and %J), this will call EnchantingDeviceConfiguration>>argMorphOf: aBlockMorph spec: specString.
Note: I am going to have to change how block morphs are created.
1) They will not all have a selector ( ~= function pointer) associated with them.
2) They need to retain the block spec in English for ease of translation.
3) They need a new field (and accessors) called peripheralType. This can be nil, but, if not, it records what sort of peripheral this block operates on.
EnchantingDeviceConfiguration>>argMorphOf: aBlockMorph spec: specString will ask the passed in block morph what its peripheralType is, and forward request to it, especially the %J and %K requests, by calling peripheralSpecificConfigurationListNumber: n.
Here are what some of the different parameters mean and what argMorphOf:spec: will do with them.
%p - Potential concrete peripherals.
On a ConfigurationBlockMorph, a %p will return a list of concrete peripherals that fill the role. argMorphOf:spec: will send EnchantingPeripheral>>concreteTypes to peripheralType to get the list and return a drop-down list. (concreteTypes on an NXTLightDetector will return #('NXT Light Sensor' 'NXT Color Sensor' 'RCX Light Sensor') as all can fulfill the role of a light detector. concreteTypes sent to a NXTRangeDetector will return #('NXT Ultrasonic Sensor') as it is the only item that detects range. )
%P - Names of applicable, configured peripherals.
On a CommandBlockMorph, a %P will return the list of configured devices that the block can operate on. For example, if a use has configured two motors, 'motor 1' and 'motor 2', and the CommandBlockMorph knows its peripheralType is an NXTTachoMotor, the EnchantingDeviceConfiguration class will ask all the configured peripherals it knows about if it acts as an NXTTachoMotor, and if so, the name will be added to the list and returned.
%J - Peripheral Specific Menu 1
%K - Peripheral Specific Menu 2
argMorphOf:spec: will look up the class of the peripheral and forward is the peripheralSpecificConfigurationListNumber: with a value of 1 for J and 2 for K. The peripheral can return whatever sort of list it would like.
Lastly, I should note that it is important for a peripheral to know its concrete subtype and the ConfigurationBlockMorph will tell it what it is.
Naming Peripherals
ConfigurationBlockMorphs in the palette pane are given a name (which shows up on the block) by send baseName to the specific subclass of EnchantingPeripheral. This will return something like 'light sensor' or 'motor'.
When the ConfigurationBlockMorph is cloned to be copied into the script area, it is given a unique name by sending uniqueName, which pass the result of sending baseName to EnchantingConfigurationDialog>>uniqueNameFor:. The unique name becomes the initialName of the ConfigurationBlockMorph, and appears on the block.
The user is then free to change names on the ConfigurationBlockMorph instances.
- If they delete a config block, the EnchantingConfigurationDialog is sent freeUpName with the name on the block.
- When they finish editing a name, the new name is updated to be the result of EnchantingConfigurationDialog>>uniqueNameFor:.
- A user can not have two peripherals with the same name.
- A user is able to change a name (removing the old one from the list) and then change it back (because the original name is now no longer on the list of names in use).
Closing the dialog
The user can close the dialog by pressing okay or cancel.
If they press cancel, the dialog disappears and nothing changes.
If they press ok, we've got a fair bit of work to do.
First, we need to update the names that appear in all the blocks in the script editor panes for every sprite (and the stage) in Enchanting. (These are not the ConfigurationBlockMorphs that appear in the EnchantingConfigurationDialog; these are all the other code blocks (derived from BlockMorph) that show up in the main user interface of Enchanting.) To do this we send updateNamesInScripts to EnchantingConfigurationDialog, which:
- goes through all of the ConfigurationBlockMorphs and maps all the old names onto new ones.
- goes through all code blocks of all sprites (and the stage), and changes any name that matches an old name to the new one, and any names that do not match an old name are blanked out, unless there is only one peripheral that would fit in the block, in which case, its name is placed there.
- it is important that a user can swap the names of devices successfully. For example, if they had a motor named 'left motor' that is now named 'right motor' and had a motor named 'right motor' that is now named 'left motor', the names should be swapped on all the regular code tiles. (The method I've outlined should do this properly.)
Next, we need to update all the EnchantingPeripheral instances that represent sensors/motors/pilots that may have been changed by the dialog that just ran. If the user ran the 'motors' dialog, then all the motors should be brought into sync with the new reality. I think the easiest way to do this is to simply delete the list of motors, then:
- for each of the three motor ports, send EnchantingConfigurationDialog>>configMorphUnderHatNumber:.
- send that ConfigurationBlockMorph createPeripheral (which, using the peripheralClass that was set upon creation, sends that class 'new' and 'takeSettingsFrom: self'). The peripheral is then returned.
Serialization (Saving) and Deserialization (Loading)
Two base classes implement the protocol used by ObjStream to serialize and deserialize object. Both EnchantingDeviceConfiguration and EnchantingPeripheral (and subclasses) will need to implement fieldsVersion, initFieldsFrom:version:, and storeFieldsOn:. The EnchantingDeviceConfiguration will be referenced from, um, to be determined (ScratchFrame)? and will be serialized with it, and so, in turn, will all the objects it references.
Displaying needed blocks
When the palettes are displayed in the main interface, they ask which blocks they should display. (Notably, ScriptableScratchMorph>>viewerPageForCategory: is sent, and sends ScriptableScratchMorph>>blocksFor: category). These functions will now be sure to send EnchantingDeviceConfiguration>>activeBlockSpecs. This returns a list of all block specifications that should be in use, so that, if a sound sensor is set up, sound sensor blocks show up in the main UI, and if no light sensor is set up, no light sensor blocks show up.
Here is psuedocode for activeBlockSpecs:
set_of_configured_classes <- self setOfConfiguredPeripheralClasses
relevant_classes <- self class orderedPeripheralClassList
make an empty list of block specifications
for each class, class1, in set_of_configured_classes
for each class, class2, in the classes relevant to this device (relevant_classes)
ask class1 if it acts as class2 (by sending 'actsAs')
if it does, ask class2 for all the blocks that it provides (by sending it 'configBlockSpec') and add those block specifications to the list
return the list of block specifications
EnchantingDeviceConfiguration>>allBlockSpecs will go through all the peripheral classes relevant to the device and ask for the block specs. This might be used in creating complete files of specs to be translated.
Drop-down lists in blocks
The blocks in the main user interface will have a drop-down in them. The type of the drop-down is represented by a single upper or lower-case letter. I intend to expand one letter to fill multiple duties. For the moment, we will assume the letter is 'P' (for peripheral). When the appropriate function (CommandBlockMorph>>uncoloredArgMorphFor: specString) is asked what to do with a 'P', we will tell it to ask the EnchantingDeviceConfiguration (by sending peripheralListFor: aCommandBlockMorph), which in turn will find out what class created the command block morph (I will likely add an attribute for that), and ask every configured device if it actsAs that class. If so, the name is added to the list.
Emitting Code
When it comes time to emit code, the EnchantingCodeExporter will ask the EnchantingDeviceConfiguration to ask all of its configured peripherals to emit configuration code (sending emitInitializationCodeOn: aStream to each one).
Note that peripherals need to keep track of their concrete type to do this. (Ex. a color sensor acting as a LightDetector needs to remember that it is a color sensor to emit the creation code).